// Copyright 2022 <mzh.scnu@qq.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package tools

import (
	"context"
	"time"

	"github.com/gofrs/uuid"
)

const (
	// defaultExp  default timeout for lock
	defaultExp = 10 * time.Second

	// sleepDur default sleep time for spin lock
	sleepDur = 10 * time.Millisecond
)

// RedisLock .
type RedisLock struct {
	Client     RedisClient
	Key        string // resources that need to be locked
	uuid       string // lock owner uuid
	cancelFunc context.CancelFunc
}

// NewRedisLock new a redis distribute lock
func NewRedisLock(client RedisClient, key string) (*RedisLock, error) {
	id, err := uuid.NewV4()
	if err != nil {
		return nil, err
	}
	return &RedisLock{
		Client: client,
		Key:    key,
		uuid:   id.String(),
	}, nil
}

// TryLock attempt to lock, return true if the lock is successful, otherwise false
func (rl *RedisLock) TryLock(ctx context.Context) (bool, error) {
	// TODO 使用redis setnx指令 uuid作为key rl.Key作为value(临界资源) 存入redis
	succ, err := rl.Client.SetNX(ctx, rl.Key, rl.uuid, defaultExp).Result()
	if err != nil || !succ {
		return false, err
	}
	//为了在成功获取锁后，设置一个后台协程，该协程定期刷新 Redis 锁的过期时间。
	//同时，为了在锁被释放或者其他原因导致不再需要刷新时，
	//可以通过调用 rl.cancelFunc() 来取消协程，释放资源，确保程序的健壮性和资源有效利用
	c, cancel := context.WithCancel(ctx)
	rl.cancelFunc = cancel
	rl.refresh(c)
	return succ, nil
}

// SpinLock Loop `retryTimes` times to call TryLock
func (rl *RedisLock) SpinLock(ctx context.Context, retryTimes int) (bool, error) {
	for i := 0; i < retryTimes; i++ {
		resp, err := rl.TryLock(ctx)
		if err != nil {
			return false, err
		}
		if resp {
			return resp, nil
		}
		time.Sleep(sleepDur)
	}
	return false, nil
}

// Unlock attempt to unlock, return true if the lock is successful, otherwise false
func (rl *RedisLock) Unlock(ctx context.Context) (bool, error) {
	resp, err := NewTools(rl.Client).Cad(ctx, rl.Key, rl.uuid)
	if err != nil {
		return false, err
	}

	if resp {
		rl.cancelFunc()
	}
	return resp, nil
}

func (rl *RedisLock) refresh(ctx context.Context) {
	go func() {
		// TODO 创建一个定时器，每隔 defaultExp / 4 的时间触发一次
		ticker := time.NewTicker(defaultExp / 4)
		for {
			select {
			// TODO 上下文结束了则退出
			case <-ctx.Done():
				return
			// TODO 每当定时器触发，刷新锁的过期时间（给这个key-value再设置一个过期时间）,保证在获取锁时，锁不会过期
			case <-ticker.C:
				rl.Client.Expire(ctx, rl.Key, defaultExp)
			}
		}
	}()
}
